Used list of priority species based on: TimeToRestore_AllPrioritySpecies_2024 google sheet (extracted on 2 April 2025) and made the following changes:
Corrected the spelling of one genus (Vernonia) and one species (cespitosa)
Updated scientific and common names based on ITIS for mistflower (Conoclinium), basket-flower (Centaurea), and Lantana; will use the common name listed in ITIS/NPN data base for these species imoving forward.
Using the common name listed in NPN database for Sambucus nigra and Passiflora incarnata, which were each listed twice in the googlesheet under two different common names.
Code
# Load list of priority speciesspp_list <-read.csv("data/ttr-priorityspecies-20250402.csv", na.strings =c(NA, ""))spp_list <- spp_list %>%mutate(across(c(common_name, scientific_name), str_trim)) %>%# Edit spelling of one genus (Vernonia) and one species (cespitosa)# Update species based on ITIS for mistflower (Conoclinium), # basket-flower (Centaurea), and Lantanamutate(scientific_name =case_when( scientific_name =="Oenothera caespitosa"~"Oenothera cespitosa", scientific_name =="Veronia gigantea"~"Vernonia gigantea", scientific_name =="Conoclinium greggii"~"Conoclinium dissectum", scientific_name =="Centaurea americana"~"Plectocephalus americanus", scientific_name =="Lantana urticoides"~"Lantana horrida", .default = scientific_name )) %>%rename(ttr_common_name = common_name)# Load information about NN speciesnn_spp <-npn_species() %>%data.frame()nn_spp <- nn_spp %>%filter(kingdom =="Plantae") %>%select(species_id, common_name, genus, species, functional_type) %>%mutate(scientific_name =paste(genus, species))# Find NN info based on scientific name of priority species (inconsistent # capitalization in priority list and a duplicate in NN database [Canada goldenrod])spp_list <- spp_list %>%left_join(nn_spp, by ="scientific_name")# Check that all priority species have a match in NN database# filter(spp_list, is.na(species_id))# Are there any duplicates? Yes# count(spp_list, species_id) %>% filter(n > 1)# filter(spp_list, species_id == 90) # In priority list, Sambucus nigra listed as both black and common elderberry# filter(spp_list, species_id == 182) # In priority list, Passiflora incarnata listed as both purple passionflower and Maypop# Remove entries for TTR priority species whose common name doesn't match NPNspp_list <- spp_list %>%filter(!ttr_common_name %in%c("Maypop", "Common elderberry"))
This left us with 53 priority species.
Downloading status-intensity data
Used rnpn package to download status and intensity data for priority species in Louisiana, New Mexico, Oklahoma, and Texas from 1 October 2023 through the present day. Downloaded observations of 4 phenophases: flowers or flower buds (flower), open flowers, fruits, and ripe fruits. Since leaves on milkweed plants may be important for monarch eggs and catepillars, we also downloaded observations of the leaves phenophase for the 8 species of milkweed on the priority list. After downloading the data, we appended information about intensity categories.
Code
# First, check that all species have these 4 phenophasesphenophases_byspp <-npn_phenophases_by_species(species_ids =c(spp_list$species_id),date ="2025-01-01") %>%data.frame()phenophases_byspp %>%group_by(species_id, species_name) %>%summarize(p500 =ifelse(500%in% phenophase_id, 1, 0),p501 =ifelse(501%in% phenophase_id, 1, 0),p516 =ifelse(516%in% phenophase_id, 1, 0),p390 =ifelse(390%in% phenophase_id, 1, 0),.groups ="keep") %>%rowwise() %>%filter(sum(c_across(p500:p390)) <4)# All species use these 4 phenophases# What phenophases do the milkweeds use?milkweed_phps <- phenophases_byspp %>%filter(str_detect(species_name, "milkweed")) %>%select(species_id, species_name, pheno_class_id, phenophase_id, phenophase_name) %>%arrange(species_name, pheno_class_id, phenophase_id)# Since leaves may be important for monarch eggs and catepillars, we may also # want to include:# 488 = Leaves (for milkweeds only)# Download and format (or load existing) NPN data for priority plant species --## Observations in 4 states (LA, NM, OK, TX)# Focus on 2025 data, but also download 2024 data (for fruits and/or comparison)phenophases <-c(500, 501, 516, 390)states4 <-c("LA", "NM", "OK", "TX")# Minor note: we could be missing observations in the four states if we use# the states argument in the download function because sometimes the state # field is missing or incorrect. Won't worry about that here, but would be # better in the long run to check state values based on lat/lonsdata_filename <-"data/ttr-data-20242025.csv"if (!file.exists(data_filename) | update_data ==TRUE) {# Download flowering, fruiting data for 2024-2025 (including 2023 so that # we can calculated individual phenometrics for 2024, if needed) status_dl <-npn_download_status_data(request_source ="erinz",years =2023:2025,species_ids = spp_list$species_id,phenophase_ids= phenophases,states = states4,additional_fields =c("observedby_person_id","partner_group","site_name", "species_functional_type")) status_dl <-data.frame(status_dl)# Download leafing data for milkweeds in 2024-2025 milkweeds <- spp_list %>%filter(str_detect(common_name, "milkweed")) %>%pull(species_id) status_mwleaf_dl <-npn_download_status_data(request_source ="erinz",years =2023:2025,species_ids = milkweeds,phenophase_ids=488,states = states4,additional_fields =c("observedby_person_id","partner_group","site_name", "species_functional_type")) status_mwleaf_dl <-data.frame(status_mwleaf_dl)# Combine everything and format status_df <-rbind(status_dl, status_mwleaf_dl) %>%mutate(obsdate =ymd(observation_date),yr =year(obsdate),php =case_when( phenophase_id ==500~"flower", phenophase_id ==501~"open flower", phenophase_id ==516~"fruit", phenophase_id ==390~"ripe fruit", phenophase_id ==488~"leaves")) %>%filter(obsdate >="2023-10-01") %>%select(-c(update_datetime, elevation_in_meters, genus, species, kingdom, phenophase_description, abundance_value, observation_date)) %>%rename(person_id = observedby_person_id,func_type = species_functional_type,lat = latitude,lon = longitude)# Write to filewrite.csv(status_df, data_filename, row.names =FALSE)# Remove objectsrm(status_df, status_dl, status_mwleaf_dl)}status <-read.csv(data_filename) %>%mutate(obsdate =ymd(obsdate),wy =ifelse(obsdate >="2024-10-01", 2025, 2024))
For the purposes of this report, we focused on observations submitted in the current water year, from 1 October 2024 - 29 March 2025, the last date that observations were available. Throughout this report, we often compared current water-year data with data collected during the previous water year (October 2023 - September 2024), while keeping in mind that we are only partially through the current water year.
Code
# Download information about intensity categoriesic <-npn_abundance_categories() %>%data.frame()ic <- ic %>%rename(intensity_category_id = category_id, intensity_value_id = value_id,intensity_name = category_name,intensity_value = value_name) %>%select(-c(category_description, value_description))# Extract just those categories that appear in status data and format:ic_subset <- ic %>%filter(intensity_category_id %in%unique(status$intensity_category_id)) %>%mutate(value1 =NA,value2 =NA,intensity_type =case_when(str_detect(intensity_value, "%") ~"percent",str_detect(intensity_value, "[0-9]") ~"number",.default ="qualitative" ))val12 <-which(colnames(ic_subset) %in%c("value1", "value2"))for (i in1:nrow(ic_subset)) {if (str_detect(ic_subset$intensity_value[i], " to ")) { ic_subset[i, val12] <-str_split_fixed(ic_subset$intensity_value[i], " to ", 2) ic_subset[i, val12] <-as.numeric(str_remove(ic_subset[i, val12], ",")) } elseif (str_detect(ic_subset$intensity_value[i], "-")) { ic_subset[i, val12] <-str_split_fixed(ic_subset$intensity_value[i], "-", 2) ic_subset[i, val12[2]] <-str_remove(ic_subset[i, val12[2]], "%") } elseif (str_detect(ic_subset$intensity_value[i], "% or more")) { ic_subset[i, val12] <-str_remove(ic_subset$intensity_value[i], "% or more") } elseif (str_detect(ic_subset$intensity_value[i], "Less than ")) { ic_subset[i, val12[1]] <-0 ic_subset[i, val12[2]] <-str_remove(ic_subset$intensity_value[i], "Less than ") ic_subset[i, val12[2]] <-str_remove(ic_subset[i, val12[2]], "%") } elseif (str_detect(ic_subset$intensity_value[i], "More than ")) { ic_subset[i, val12] <-str_remove(ic_subset$intensity_value[i], "More than ") ic_subset[i, val12[1]] <-as.numeric(str_remove(ic_subset[i, val12[1]], ",")) +1 ic_subset[i, val12[2]] <-as.numeric(str_remove(ic_subset[i, val12[2]], ",")) +1 }}ic_subset <- ic_subset %>%mutate_at(c("value1", "value2"), as.numeric)# Assigning a middle-ish value for each range (keeping it to nice numbers like # 5, 50, 500, and 5000)ic_subset <- ic_subset %>%mutate(mag =nchar(value1) -1) %>%mutate(value =case_when( value1 == value2 ~round(value1), intensity_type =="number"& value1 ==0~1, intensity_type =="number"& value1 !=0~round_any(rowMeans(across(value1:value2)), 5* (10^ mag)), intensity_type =="percent"~round(rowMeans(across(value1:value2))),.default =NA )) %>%select(-c(mag, value1, value2))ic_append <- ic_subset %>%select(intensity_category_id, intensity_name, intensity_value, value) %>%rename(intensity_cat = intensity_value, intensity = value)status <- status %>%left_join(ic_append, by =c("intensity_category_id", "intensity_value"="intensity_cat")) %>%select(-intensity_category_id)
Identifying issues related to reported intensity values
There was 1 instance since October 2024 where an observer did not report “yes” for a particular phenohpase, but did report an intensity value.
Table 1: Observations with an intensity value, where phenophase state was not positive.
Site name
State
Species
Observation date
Phenophase
Status
Intensity
Bayton Loop Preserve
TX
American beautyberry
2024-12-21
ripe fruit
-1
Less than 5%
To explore whether observers were counting the number of flowers rather than the number of inflorescences, we identified observations where the reported intensity value was in the highest two categories available for that species.
Table 2: Observations of flowers with high reported intensity values by water year.
Species
Phenophase
Intensity category
No. 2024 observations
No. 2024 plants
No. 2025 observations
No. 2025 plants
American beautyberry
flower
1,001 to 10,000
4
4
0
0
Texas lupine
flower
101 to 1,000
0
0
1
1
button eryngo
flower
101 to 1,000
4
1
0
0
cardinalflower
flower
101 to 1,000
1
1
0
0
common buttonbush
flower
1,001 to 10,000
15
6
0
0
eastern baccharis
flower
1,001 to 10,000
6
3
1
1
eastern baccharis
flower
More than 10,000
0
0
1
1
eastern redbud
flower
1,001 to 10,000
4
3
14
4
horsetail milkweed
flower
101 to 1,000
3
1
0
0
purple passionflower
flower
101 to 1,000
2
1
0
0
red maple
flower
1,001 to 10,000
75
13
88
14
red maple
flower
More than 10,000
6
1
2
1
rubber rabbitbrush
flower
1,001 to 10,000
12
3
2
2
rubber rabbitbrush
flower
More than 10,000
8
4
0
0
seaside goldenrod
flower
101 to 1,000
0
0
2
1
white crownbeard
flower
101 to 1,000
0
0
2
1
wild bergamot
flower
101 to 1,000
1
1
0
0
Identifying inconsistent phenophase status reports
We wanted to identify when observers provided incompatible status reports for different phenophases. In particular, we identified when observers reported a “no” to flowers but reported a “yes” or “?” to open flowers. Similarly, we identified when observers reported a “no” to fruits but reported a “yes” or “?” to ripe fruits. We also identified when observers reported a “?” to flowers but reported a “yes” to open flowers, and when observers reported a “?” to fruits but reported a “yes” to ripe fruits.
Code
# To look at this, can't have more than one observation of a plant per person# per day. We've already removed duplicates, but now need to resolve instances # where somebody made multiple observations of the same plant on the same date # that differed in some way.# For now, will keep record with more advanced phenophase or higher # intensity value. Will do this by sorting observations in descending# order and keeping only the first inddateobsp <- status %>%group_by(common_name, individual_id, obsdate, person_id, php) %>%summarize(n_obs =n(),.groups ="keep") %>%data.frame() inddateobsp$obsnum <-1:nrow(inddateobsp) status <- status %>%arrange(person_id, individual_id, obsdate, php, desc(phenophase_status), desc(intensity)) %>%left_join(select(inddateobsp, -c(n_obs, common_name)), by =c("person_id", "individual_id", "obsdate", "php")) %>%# Create "dups" column, where dups > 1 indicates that the observation can be# removed since there's another observation that same day with more advanced# phenology or higher intensity/abundance.mutate(dups =sequence(rle(as.character(obsnum))$lengths))# Remove extra observations and unnecessary columns status <- status %>%filter(dups ==1) %>%select(-c(obsnum, dups)) %>%arrange(common_name, obsdate, person_id, php)# To identify inconsistent status values, will need to put flower/fruit data # into wide form (all data for a plant visit in the same row). Removing# unknown status observations first (<0.5% of fruit/flower observations).statusw <- status %>%filter(php !="leaves") %>%filter(phenophase_status !=-1) %>%select(person_id, partner_group, site_id, state, common_name, individual_id, wy, obsdate, php, phenophase_status, intensity) %>%rename(status = phenophase_status) %>%pivot_wider(names_from = php,names_glue ="{php}_{.value}",values_from =c(status, intensity)) %>%data.frame()# Identify phenophase status inconsistencies# NOTE: changing NAs to 999 in order to make this code simplerstatusw <- statusw %>%mutate(across(contains("status"), ~replace_na(., 999))) %>%# Problem: flower = 0, open = NA or 1mutate(flower0_openNot0 =ifelse(flower_status ==0& open.flower_status !=0, 1, 0)) %>%# Problem: flower = NA, open = 1mutate(flowerNA_open1 =ifelse(flower_status ==999& open.flower_status ==1,1, 0)) %>%# Problem: fruit = 0, ripe = NA or 1mutate(fruit0_ripeNot0 =ifelse(fruit_status ==0& ripe.fruit_status !=0, 1, 0)) %>%# Problem: fruit = NA, ripe = 1mutate(fruitNA_ripe1 =ifelse(fruit_status ==999& ripe.fruit_status ==1,1, 0))# Table summarizing problems with flower/open flower statusflower_probs <- statusw %>%group_by(common_name, wy) %>%summarize(n =n(),n_flower0 =sum(flower_status ==0),n_flower1 =sum(flower_status ==1),n_flowerNA =sum(flower_status ==999),n_open0 =sum(open.flower_status ==0),n_open1 =sum(open.flower_status ==1),n_openNA =sum(open.flower_status ==999),n_flower0_openNot0 =sum(flower0_openNot0 ==1),n_flowerNA_open1 =sum(flowerNA_open1 ==1),.groups ="keep") %>%filter(n_flower0_openNot0 + n_flowerNA_open1 >0) %>%data.frame()# Table summarizing problems with fruit/ripe fruit statusfruit_probs <- statusw %>%group_by(common_name, wy) %>%summarize(n =n(),n_fruit0 =sum(fruit_status ==0),n_fruit1 =sum(fruit_status ==1),n_fruitNA =sum(fruit_status ==999),n_ripe0 =sum(ripe.fruit_status ==0),n_ripe1 =sum(ripe.fruit_status ==1),n_ripeNA =sum(ripe.fruit_status ==999),n_fruit0_ripeNot0 =sum(fruit0_ripeNot0 ==1),n_fruitNA_ripe1 =sum(fruitNA_ripe1 ==1),.groups ="keep") %>%filter(n_fruit0_ripeNot0 + n_fruitNA_ripe1 >0) %>%data.frame()
Table 3: Inconsistent reports of flowering phenophase statuses by species and water year
Species
Water year
No. observations
Flowers:no AND Open flowers:yes/?
Flowers:? AND Open flowers:yes
American beautyberry
2024
585
0
28
American beautyberry
2025
386
2
0
common buttonbush
2024
300
2
0
common buttonbush
2025
163
1
0
common sunflower
2024
156
1
0
eastern baccharis
2025
64
1
0
eastern purple coneflower
2024
34
1
0
eastern redbud
2024
462
4
2
horsetail milkweed
2024
147
1
0
purple prairie clover
2024
250
0
17
red maple
2024
497
1
0
red maple
2025
291
2
0
rubber rabbitbrush
2024
178
3
0
wax mallow
2025
27
1
0
Table 4: Inconsistent reports of fruiting phenophase statuses
Species
Water year
No. observations
Fruit:no AND Ripe fruit:yes/?
Fruit:? AND Ripe fruit:yes
American star-thistle
2025
7
1
0
blackeyed Susan
2024
1
1
0
blackeyed Susan
2025
7
1
0
common buttonbush
2024
300
1
0
common sunflower
2024
156
4
0
common sunflower
2025
53
1
0
eastern baccharis
2024
114
1
0
eastern redbud
2024
462
1
0
eastern redbud
2025
206
2
0
horsetail milkweed
2024
147
1
0
horsetail milkweed
2025
40
1
0
purple passionflower
2025
12
1
0
rubber rabbitbrush
2025
59
1
0
white crownbeard
2025
75
1
0
Summary of sites monitored
Code
# Plot locations where flowering/fruiting or milkweed leaves observed in current# water year (Oct 2024 and present)status <- status %>%mutate(ind_date =paste0(individual_id, "_", obsdate)) locs <- status %>%filter(wy ==2025) %>%group_by(site_id, lat, lon, state) %>%summarize(nspp =n_distinct(common_name),nplants =n_distinct(individual_id),nobservers =n_distinct(person_id),# nobs: Number of observations, where an observations is all# data (all phenophases) submitted for a plant on given datenobs =n_distinct(ind_date), .groups ="keep") %>%data.frame()locs_by_state_int <- locs %>%group_by(state) %>%summarize(nspp_per_site =round(mean(nspp), 2),nplants_per_site =round(mean(nplants), 2),nobs_per_site =round(mean(nobs),2)) %>%data.frame()locs_by_state <- status %>%filter(wy ==2025) %>%group_by(state) %>%summarize(nsites =n_distinct(site_id),nspp =n_distinct(common_name),nplants =n_distinct(individual_id),nobs =n_distinct(ind_date)) %>%data.frame() %>%left_join(locs_by_state_int, by ="state")
Figure 1: Locations of sites where priority species were monitored between October 2024 and present.
Figure 2: Locations of sites in Louisiana where priority species were monitored between October 2024 and present.
Figure 3: Locations of sites in New Mexico where priority species were monitored between October 2024 and present.
Figure 4: Locations of sites in Oklahoma where priority species were monitored between October 2024 and present.